pacenotes_installer\wizard\scene/
download_tree_view.rs1use windows_sys::Win32::Foundation::{HWND, LPARAM, WPARAM};
5use windows_sys::Win32::UI::Controls as ctl;
6use windows_sys::Win32::UI::WindowsAndMessaging as wm;
7
8const TVM_INSERTITEMW: u32 = 0x1132;
9const TVM_DELETEITEM: u32 = 0x1101;
10const TVM_SETITEMW: u32 = 0x113F;
11const TVM_SETEXTENDEDSTYLE: u32 = 0x112C;
12const TVI_ROOT: isize = -0x10000;
13const TVI_LAST: isize = -0x0FFFE;
14pub const TVN_ITEMCHANGEDW: u32 = (-417i32) as u32;
16pub const WM_SYNC_CHECKS: u32 = wm::WM_APP + 10;
18
19const STATE_IMAGE_SHIFT: u32 = 12;
21const STATE_IMAGE_UNCHECKED: u32 = 1;
22const STATE_IMAGE_CHECKED: u32 = 2;
23const STATE_IMAGE_PARTIAL: u32 = 3;
24
25#[repr(C)]
27struct NmTvItemChange {
28 header: ctl::NMHDR,
29 change: u32,
30 item: *mut core::ffi::c_void,
31 state_new: u32,
32 state_old: u32,
33 lparam: LPARAM,
34}
35
36pub unsafe fn enable_partial_checkboxes(dialog: HWND) {
41 let control = tree_control(dialog);
42 wm::SendMessageW(
43 control,
44 TVM_SETEXTENDEDSTYLE,
45 ctl::TVS_EX_PARTIALCHECKBOXES as WPARAM,
46 ctl::TVS_EX_PARTIALCHECKBOXES as LPARAM,
47 );
48 let style = wm::GetWindowLongPtrW(control, wm::GWL_STYLE);
49 wm::SetWindowLongPtrW(control, wm::GWL_STYLE, style | ctl::TVS_CHECKBOXES as isize);
50}
51
52pub unsafe fn populate(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
54 let control = tree_control(dialog);
55 state.tree_syncing = true;
56 wm::SendMessageW(control, TVM_DELETEITEM, 0, TVI_ROOT);
57 state.tree_items = vec![0isize; state.tree.len()];
58
59 for root in state.tree.roots().to_vec() {
60 insert_node(control, state, root, TVI_ROOT);
61 }
62 refresh_checks(dialog, state);
63 state.tree_syncing = false;
64}
65
66unsafe fn insert_node(
67 control: HWND,
68 state: &mut crate::wizard::state::WizardState,
69 node: usize,
70 parent: isize,
71) {
72 let text = crate::i18n::to_wide(&row_label(state, node));
73 let mut item: ctl::TVINSERTSTRUCTW = core::mem::zeroed();
74 item.hParent = parent;
75 item.hInsertAfter = TVI_LAST;
76 item.Anonymous.itemex.mask = ctl::TVIF_TEXT | ctl::TVIF_PARAM | ctl::TVIF_STATE;
77 item.Anonymous.itemex.pszText = text.as_ptr() as *mut u16;
78 item.Anonymous.itemex.lParam = node as LPARAM;
79 item.Anonymous.itemex.stateMask = ctl::TVIS_EXPANDED;
82 item.Anonymous.itemex.state = ctl::TVIS_EXPANDED;
83
84 let handle = wm::SendMessageW(control, TVM_INSERTITEMW, 0, &item as *const _ as LPARAM);
85 state.tree_items[node] = handle;
86 for child in state.tree.children(node).to_vec() {
87 insert_node(control, state, child, handle);
88 }
89}
90
91fn row_label(state: &crate::wizard::state::WizardState, node: usize) -> String {
94 let node = state.tree.node(node);
95 let suffix = match (node.installed, node.state) {
96 (_, None) | (false, Some(pacenotes_installer::tree::LeafState::NotCached)) => {
97 return node.label.clone()
98 }
99 (_, Some(pacenotes_installer::tree::LeafState::Unavailable)) => {
100 crate::i18n::IDS_STATE_UNAVAILABLE
101 }
102 (true, Some(_)) => crate::i18n::IDS_STATE_INSTALLED,
103 (false, Some(pacenotes_installer::tree::LeafState::Downloaded)) => {
104 crate::i18n::IDS_STATE_DOWNLOADED
105 }
106 (false, Some(pacenotes_installer::tree::LeafState::UpdateAvailable)) => {
107 crate::i18n::IDS_STATE_UPDATE
108 }
109 (false, Some(pacenotes_installer::tree::LeafState::LocalOnly)) => {
110 crate::i18n::IDS_STATE_LOCAL
111 }
112 };
113 format!("{} {}", node.label, crate::i18n::load_string(suffix))
114}
115
116pub unsafe fn refresh_checks(dialog: HWND, state: &mut crate::wizard::state::WizardState) {
118 let control = tree_control(dialog);
119 let was_syncing = state.tree_syncing;
120 state.tree_syncing = true;
121 for node in 0..state.tree_items.len().min(state.tree.len()) {
123 let handle = state.tree_items[node];
124 if handle == 0 {
125 continue;
126 }
127 let image = match state.tree.check_of(node) {
128 pacenotes_installer::tree::Check::Unchecked => STATE_IMAGE_UNCHECKED,
129 pacenotes_installer::tree::Check::Checked => STATE_IMAGE_CHECKED,
130 pacenotes_installer::tree::Check::Partial => STATE_IMAGE_PARTIAL,
131 };
132 let mut item: ctl::TVITEMEXW = core::mem::zeroed();
133 item.mask = ctl::TVIF_HANDLE | ctl::TVIF_STATE;
134 item.hItem = handle;
135 item.stateMask = ctl::TVIS_STATEIMAGEMASK;
136 item.state = image << STATE_IMAGE_SHIFT;
137 wm::SendMessageW(control, TVM_SETITEMW, 0, &item as *const _ as LPARAM);
138 }
139 state.tree_syncing = was_syncing;
140}
141
142pub unsafe fn on_item_changed(dialog: HWND, lparam: LPARAM) -> isize {
145 let Some(state) = crate::wizard::page::state(dialog) else {
146 return 0;
147 };
148 if state.tree_syncing {
149 return 0;
150 }
151 let Some(change) = (lparam as *const NmTvItemChange).as_ref() else {
152 return 0;
153 };
154 let node = change.lparam as usize;
156 if state.tree_items.get(node).copied() != Some(change.item as isize) {
157 return 0;
158 }
159 let image = |state: u32| (state & ctl::TVIS_STATEIMAGEMASK) >> STATE_IMAGE_SHIFT;
160 let checked = image(change.state_new) == STATE_IMAGE_CHECKED
163 || image(change.state_old) == STATE_IMAGE_PARTIAL;
164 state.tree.set_checked(node, checked);
165 wm::PostMessageW(dialog, WM_SYNC_CHECKS, 0, 0);
167 0
168}
169
170unsafe fn tree_control(dialog: HWND) -> HWND {
171 wm::GetDlgItem(dialog, crate::wizard::control_ids::IDC_S4_TREE as i32)
172}